// Copyright 2019 The Go Authors. All rights reserved.
// Use of this session code is governed by a BSD-style
// license that can be found in the LICENSE file.

package span

import (
	"fmt"
	"unicode/utf16"
	"unicode/utf8"
)

// Span represents a session code range in standardized form.
type UTF16Span struct {
	Start UTF16Position
	End   UTF16Position
}

func (s UTF16Span) IsValid() bool {
	return s.Start.IsValid() && s.End.IsValid()
}

func (s UTF16Span) ToSpan(content []byte) (Span, error){
	return s.ToSpanWithCache(content, GetLineStarts(content))
}

func (s UTF16Span) ToSpanWithCache(content []byte, lineStarts []int) (Span, error){
	start, err := s.Start.ToPositionWithCache(content, lineStarts)
	if err != nil {
		return Span{}, err
	}
	end, err := s.End.ToPositionWithCache(content, lineStarts)
	if err != nil {
		return Span{}, err
	}
	return Span{Start: start, End: end}, nil
}

// Position represents a single point within a file.
// In general this should only be used as part of a Span, as on its own it
// does not carry enough information.
type UTF16Position struct {
	Line   int
	Column int
}

func (p UTF16Position) IsValid() bool {
	return p.Line >= 0 && p.Column >= 0
}

func (p UTF16Position) ToOffset(content []byte) (int, error) {
	return p.ToOffsetWithCache(content, GetLineStarts(content))
}

func (p UTF16Position) ToOffsetWithCache(content []byte, lineStarts []int) (int, error) {
	point, err := p.ToPositionWithCache(content, lineStarts)
	if err != nil {
		return -1, err
	}
	return point.ToOffsetWithCache(lineStarts)
}

func (p UTF16Position) ToPosition(content []byte) (Position, error) {
	return FromUTF16Position(p, content, GetLineStarts(content))
}

func (p UTF16Position) ToPositionWithCache(content []byte, lineStarts []int) (Position, error) {
	return FromUTF16Position(p, content, lineStarts)
}

// ToUTF16Position calculates the utf16 column expressed by the point given the
// supplied file contents.
// This is used to convert from the native (always in bytes) column
// representation and the utf16 counts used by some editors.
func ToUTF16Position(p Position, content []byte, lineStarts []int) (UTF16Position, error) {
	if !p.IsValid() {
		return UTF16Position{}, fmt.Errorf("ToUTF16Position: position is invalid(%d, %d)", p.Line, p.Column)
	}
	if p.Line > len(lineStarts)-1 {
		return UTF16Position{}, fmt.Errorf("ToUTF16Position: line(%d) out of range(%d)", p.Line, len(lineStarts)-1)
	}
	if p.Column == 0 {
		// 0-based column 0, so it must be chr 1
		return UTF16Position{Line: p.Line, Column: p.Column}, nil
	}
	if lineStarts[p.Line] + p.Column > len(content) {
		return UTF16Position{}, fmt.Errorf("ToUTF16Position: position(line:%d column:%d offset:%d) out of content(%d)",
			p.Line, p.Column, lineStarts[p.Line] + p.Column, len(content))
	}

	var columnLength = 0
	if p.Line == len(lineStarts) - 1 {
		columnLength = len(content) - lineStarts[p.Line]
	} else {
		columnLength = lineStarts[p.Line+1] - lineStarts[p.Line]
	}
	if p.Column > columnLength {
		return UTF16Position{}, fmt.Errorf("ToUTF16Position: column(%d) out of range(%d)", p.Column, columnLength)
	}



	var lineStart = lineStarts[p.Line]
	// Use the offset to pick out the line start.
	// This cannot panic: offset > len(content) and lineOffset < offset.
	start := content[lineStart:]

	// Now, truncate down to the supplied column.
	start = start[:p.Column]

	// and count the number of utf16 characters
	// in theory we could do this by hand more efficiently...
	var column = len(utf16.Encode([]rune(string(start))))
	return UTF16Position{Line: p.Line, Column: column}, nil
}

// FromUTF16Position advances the point by the utf16 character offset given the
// supplied line contents.
// This is used to convert from the utf16 counts used by some editors to the
// native (always in bytes) column representation.
func FromUTF16Position(p UTF16Position, content []byte, lineStarts []int) (Position, error) {
	if !p.IsValid() {
		return Position{}, fmt.Errorf("FromUTF16Position: position is invalid(%d, %d)", p.Line, p.Column)
	}
	if p.Line > len(lineStarts) - 1 {
		return Position{}, fmt.Errorf("FromUTF16Position: line(%d) out of range(%d)", p.Line, len(lineStarts)-1)
	}

	if p.Column == 0 {
		return Position{Line: p.Line, Column: p.Column}, nil
	}

	remains := content[lineStarts[p.Line]:]
	offset := 0
	// scan forward the specified number of characters
	for count := 0; count < p.Column; count++ {
		if len(remains) <= 0 {
			return Position{}, fmt.Errorf("FromUTF16Position: position out of content")
		}
		r, w := utf8.DecodeRune(remains)
		if r == '\n' {
			// Per the LSP spec:
			//
			// > If the character value is greater than the line length it
			// > defaults back to the line length.
			break
		}
		remains = remains[w:]
		if r >= 0x10000 {
			// a two point rune
			count++
			// if we finished in a two point rune, do not advance past the first
			if count >= p.Column {
				break
			}
		}
		offset += w
	}

	return Position{Line: p.Line, Column: offset}, nil
}
